Future.js ➔ ... ➔ this.then   B
last analyzed

Complexity

Conditions 5
Paths 20

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
c 1
b 0
f 0
nc 20
nop 2
dl 0
loc 19
rs 8.8571
1
var Race = require('./Race').Race
2
3
/**
4
 * @callback Future~resolver
5
 *
6
 * @param {Function} resolve
7
 * @param {Function} reject
8
 */
9
10
/**
11
 * @enum
12
 * @readonly
13
 */
14
var Status = {
15
  Pending: 0,
16
  Resolving: 1,
17
  Rejected: 2,
18
  Fulfilled: 3
19
}
20
21
Status.name = function (value) {
22
  return Object.keys(Status).reduce(function (carrier, key) {
23
    return carrier || (Status[key] === value ? key : null)
24
  }, null)
25
}
26
27
/**
28
 * This is a very standard promise interface implementation, but with ability
29
 * to be externally completed or cancelled by `#resolve()` and `#reject()`
30
 * methods.
31
 *
32
 * It is named after Java's CompletableFuture.
33
 *
34
 * **NB:** this implementation doesn't account for cyclic references (which are
35
 * tremendously easy to implement).
36
 *
37
 * @class
38
 * @template T
39
 *
40
 * @param {Future~resolver} [resolver] Standard promise resolver
41
 */
42
function Future (resolver) {
43
  var self = this
44
  var status = Status.Pending
45
  var identity
46
  var propagation = null
47
  var queue = []
48
49
  this.getValue = function () { return identity }
50
51
  this.getStatus = function () { return status }
52
53
  this.hasStatus = function (s) { return status === s }
54
55
  this.isPending = function () { return self.hasStatus(Status.Pending) }
56
  this.isFulfilled = function () { return self.hasStatus(Status.Fulfilled) }
57
  this.isRejected = function () { return self.hasStatus(Status.Rejected) }
58
  this.isResolved = function () {
59
    return status === Status.Fulfilled || status === Status.Rejected
60
  }
61
62
  function schedulePropagation () {
63
    // if already scheduled
64
    if (propagation) { return }
65
    // @see https://promisesaplus.com/#point-34
66
    // setTimeout is used because process.nextTick is not something one mocks
67
    propagation = setTimeout(function () {
68
      propagation = null
69
      propagate()
70
    }, 0)
71
  }
72
73
  function propagate () {
74
    if (!self.isResolved()) { return }
75
    var staged = queue
76
    queue = []
77
    staged.forEach(function (callback) {
78
      callback(status, identity)
79
    })
80
  }
81
82
  function setIdentity (nextStatus, value) {
83
    if (self.isResolved()) { return }
84
    status = nextStatus
85
    identity = value
86
    schedulePropagation()
87
  }
88
89
  var fulfill = setIdentity.bind(self, Status.Fulfilled)
90
  var reject = setIdentity.bind(self, Status.Rejected)
91
92
  function resolve (nextStatus, value) {
93
    if (!self.isPending()) { return self }
94
    extendedResolutionProcedure(nextStatus, value)
95
    return self
96
  }
97
98
  /**
99
   * Resolves (fulfills) current instance with provided value
100
   *
101
   * @param {T} [value]
102
   * @returns {Future.<T>} Current instance
103
   */
104
  this.fulfill = resolve.bind(this, Status.Fulfilled)
105
106
  this.resolve = this.fulfill
107
108
  /**
109
   * Rejects current instance with provided value
110
   *
111
   * @param {T} [value]
112
   * @returns {Future.<T>} Current instance
113
   */
114
  this.reject = resolve.bind(this, Status.Rejected)
115
116
  if (resolver) { resolver(this.resolve, this.reject) }
117
118
  /**
119
   * @param x
120
   */
121
  function promiseResolutionProcedure (x) {
122
    if (self.isResolved()) { return }
123
    status = Status.Resolving
124
    if (x === self) {
125
      var message = 'Can\'t resolve promise with itself'
126
      return setIdentity(Status.Rejected, new TypeError(message))
127
    }
128
    if (x instanceof Future && x.isResolved()) {
129
      return extendedResolutionProcedure(x.getStatus(), x.getValue())
130
    }
131
    if (!x || (typeof x !== 'function' && typeof x !== 'object')) {
132
      return fulfill(x)
133
    }
134
    var race = new Race(1)
135
    var resolvePromise = race.racer(promiseResolutionProcedure)
136
    var rejectPromise = race.racer(reject)
137
    try {
138
      var then = x.then
139
      if (typeof then !== 'function') { return fulfill(x) }
140
      then.call(x, resolvePromise, rejectPromise)
141
    } catch (e) {
142
      rejectPromise(e)
143
    }
144
  }
145
146
  function extendedResolutionProcedure (status, x) {
147
    status === Status.Rejected ? reject(x) : promiseResolutionProcedure(x)
148
    return self
149
  }
150
151
  /**
152
   * Standard then-implementation
153
   *
154
   * @param {Function} [onFulfill]
155
   * @param {Function} [onReject]
156
   * @returns {Future.<T>}
157
   */
158
  this.then = function (onFulfill, onReject) {
159
    if (typeof onFulfill !== 'function') { onFulfill = null }
160
    if (typeof onReject !== 'function') { onReject = null }
161
    if (!onFulfill && !onReject) { return self }
162
    onReject = onReject || function (error) { throw error }
163
    onFulfill = onFulfill || function (value) { return value }
164
    var target = new Future()
165
    var callback = function (status, value) {
166
      var handler = status === Status.Fulfilled ? onFulfill : onReject
167
      try {
168
        target.resolve(handler(value))
169
      } catch (e) {
170
        target.reject(e)
171
      }
172
    }
173
    queue.push(callback)
174
    schedulePropagation()
175
    return target
176
  }
177
178
  /**
179
   * Attaches current promise to provided thenable, accepting it's eventual
180
   * outcome.
181
   *
182
   * @param {Thenable} promise
183
   *
184
   * @return {Future}
185
   */
186
  this.wrap = function (promise) {
187
    promise.then(function (value) {
188
      self.fulfill(value)
189
    }, function (e) {
190
      self.reject(e)
191
    })
192
    return self
193
  }
194
195
  this.toString = function () {
196
    var state = Status.name(status)
197
    return 'Future <' + state + (identity ? ':' + identity : '') + '>'
198
  }
199
}
200
201
/**
202
 * Returns promise that awaits all passed promises.
203
 *
204
 * @param {Array.<Promise.<*>|Thenable<*>>} promises
205
 * @returns {Future.<*>}
206
 */
207
Future.all = function (promises) {
208
  return Future.wrap(Promise.all(promises))
209
}
210
211
/**
212
 * Returns result of first resolved promise, be it fulfillment or rejection
213
 *
214
 * @param {Array.<Promise.<*>|Thenable<*>>} promises
215
 * @returns {Future.<*>}
216
 */
217
Future.race = function (promises) {
218
  return Future.wrap(Promise.race(promises))
219
}
220
221
/**
222
 * Returns resolved promise.
223
 *
224
 * @param {*} [value]
225
 * @returns {Future.<*>}
226
 */
227
Future.resolve = function (value) {
228
  return new Future().resolve(value)
229
}
230
231
/**
232
 * Returns rejected promise.
233
 *
234
 * @param {*} [value]
235
 * @returns {Future.<*>}
236
 */
237
Future.reject = function (value) {
238
  return new Future().reject(value)
239
}
240
241
/**
242
 * Wraps given promise in a Future, giving user code an option
243
 * to reject/resolve it.
244
 *
245
 * @param {Promise.<*>|Thenable.<*>} promise
246
 * @returns {Future.<*>}
247
 */
248
Future.wrap = function (promise) {
249
  return new Future().wrap(promise)
250
}
251
252
Future.Status = Status
253
254
module.exports = {
255
  Future: Future
256
}
257